package com.letsgo;

import java.util.ArrayList;

/**
 * Class used to represent the Board of a game
 */
public class Board {
	private int width ;
	private int height ;
	private BoardNode[][] grid ;
	private boolean nextPlayerWhite = true;
	
	/**
	 * Board constructor which creates a grid with the given width and height
	 * @param width
	 * @param height
	 */
	public Board(int width, int height)
	{
		this.width = width ;
		this.height = height ;
		this.grid = new BoardNode[this.width][this.height];
		for(int i=0 ; i<this.width ; i++)
		{
			for(int j=0 ; j<this.height ; j++)
			{
				this.grid[i][j] = new BoardNode(i,j);
			}
		}
	}
	
	/**
	 * Basic constructor which creates a grid 9x9
	 */
	public Board()
	{
		this(9,9);
	}
	
	/**
	 * Return a String filled with the coordinates of each BoardNode and its type
	 * ex : 3.4-BLACK
	 */
	public String toString()
	{
		String s = "";
		
		if(this.grid != null)
		{
			for(int i=0 ; i<this.width ; i++)
			{
				for(int j=0 ; j<this.height ; j++)
				{
					s += " "+this.grid[i][j].getRow()+"."+this.grid[i][j].getCol()+"-"+this.grid[i][j].getType();
				}
			}
		}
		
		return s ;
	}
	
	/**
	 * Fills the board from an ArrayList of BoardMovement (recalculates the game)
	 * @param bms ArrayList of BoardMovement, it must start from seq 1
	 * @throws BoardBadHistory if the BoardMovement's sequence number are not following each other or if the first is not 1
	 */
	public void fillFromHistory(ArrayList<BoardMovement> bms) throws BoardBadHistory
	{
		if(bms == null)
			return ;
		
		if(bms.get(0).getSeq() > 1)
			throw new BoardBadHistory();
		
		int seq = 1 ;
		BNT currentType ;
		
		for(BoardMovement bm : bms)
		{
			// If the sequence number are not in order throw an error
			if(bm.getSeq() != seq)
				throw new BoardBadHistory();
			
			// Setting the current colors for the turn
			if(bm.getSeq()%2 == 0)
			{
				currentType = BNT.WHITE;
			}
			else
			{
				currentType = BNT.BLACK;
			}
			
			// If the node is already filled an error is thrown
			if(this.grid[bm.getRow()][bm.getColumn()].getType().equals(BNT.BLACK) || this.grid[bm.getRow()][bm.getColumn()].getType().equals(BNT.WHITE))
				throw new BoardBadHistory();
			else
			{
				// Setting the new color of the node
				this.grid[bm.getRow()][bm.getColumn()].setType(currentType);
			}

			// Check if some tokens are deleted due to the last movement
			checkArea(bm.getRow(), bm.getColumn());
			
			seq++ ;
			
		}
		this.nextPlayerWhite = (seq % 2 == 1);
	}
	
	/**
	 * Start a check around the node given, this function should be called after each movement
	 * it will check if some opponent's tokens need to be removed
	 * @param row of the last movement
	 * @param col of the last movement
	 * @see checkFleeWay
	 */
	private void checkArea(int row, int col)
	{
		if(row < 0 || col < 0 || row > width || col > height)
			return ;
		
		BNT currentType = this.grid[row][col].getType() ;
		BNT currentOpponentType ;
		
		if(currentType.equals(BNT.BLACK))
			currentOpponentType = BNT.WHITE ;
		else if(currentType.equals(BNT.WHITE))
			currentOpponentType = BNT.BLACK ;
		else
			return ;
	
		// CHECK TOP
		if(row > 0 && this.grid[row-1][col].getType().equals(currentOpponentType))
		{
			checkFleeWay(row-1, col, currentType, currentOpponentType);
		}
		
		// CHECK RIGHT
		if(col < this.width-1 && this.grid[row][col+1].getType().equals(currentOpponentType))
		{
			checkFleeWay(row, col+1, currentType, currentOpponentType);
		}
		
		// CHECK DOWN
		if(row < this.height-1 && this.grid[row+1][col].getType().equals(currentOpponentType))
		{
			checkFleeWay(row+1, col, currentType, currentOpponentType);
		}
		
		// CHECK LEFT
		if(col > 0 && this.grid[row][col-1].getType().equals(currentOpponentType))
		{
			checkFleeWay(row, col-1, currentType, currentOpponentType);
		}
	}
	
	/**
	 * Checks the Area around the given node, if the node has a color it searchs if a flee way is available
	 * if there is no flee way available, the funcion removes all the nodes from the chain linked to the given coordinates
	 * @param row of the node to check
	 * @param col of the node to check
	 * @param currentType color of the last movement
	 * @param currentOpponentType color of the opponent
	 * @see checkArea
	 */
	private void checkFleeWay(int row, int col, BNT currentType, BNT currentOpponentType)
	{		
		// This ArrayList is used to know which nodes were visited during the search
		// if the search finished without finding an empty node around the chain, 
		// all the visited nodes will be deleted
		ArrayList<BoardNode> visited = new ArrayList<BoardNode>();
		
		// This ArrayList is used to know which nodes are part of the chain, and need to be visited
		// the search finish when this list is empty
		ArrayList<BoardNode> unvisited = new ArrayList<BoardNode>() ;
		
		BoardNode current ;
		int cRow ;
		int cCol ;
		
		boolean fleeWay = false ;
		unvisited.add(this.grid[row][col]);
		while(!unvisited.isEmpty() && !fleeWay)
		{
			current = unvisited.remove(0);
			visited.add(current);
			cRow = current.getRow();
			cCol = current.getCol();
			
			if(cRow > 0) // Top
			{
				if(this.grid[cRow-1][cCol].getType().equals(BNT.EMPTY))
					fleeWay = true ; // An empty node was found
				else if(this.grid[cRow-1][cCol].getType().equals(currentOpponentType))
					unvisited.add(this.grid[cRow-1][cCol]); // An opponent token was found, so we add it to the unvisited list
			}
			
			if(cCol < this.width-1) // Right
			{
				if(this.grid[cRow][cCol+1].getType().equals(BNT.EMPTY))
					fleeWay = true ; // An empty node was found
				else if(this.grid[cRow][cCol+1].getType().equals(currentOpponentType))
					unvisited.add(this.grid[cRow][cCol+1]); // An opponent token was found, so we add it to the unvisited list
			}
			
			if(cRow < this.height-1) // Down
			{
				if(this.grid[cRow+1][cCol].getType().equals(BNT.EMPTY))
					fleeWay = true ; // An empty node was found
				else if(this.grid[cRow+1][cCol].getType().equals(currentOpponentType))
					unvisited.add(this.grid[cRow+1][cCol]); // An opponent token was found, so we add it to the unvisited list
			}
			
			if(cCol > 0) // Left
			{
				if(this.grid[cRow][cCol-1].getType().equals(BNT.EMPTY))
					fleeWay = true ; // An empty node was found
				else if(this.grid[cRow][cCol-1].getType().equals(currentOpponentType))
					unvisited.add(this.grid[cRow][cCol-1]); // An opponent token was found, so we add it to the unvisited list
			}
		}
		
		
		// If there is no fleeWay at the end of the search, all the visited nodes are deleted
		if(!fleeWay)
		{
			// Remove BoardNode from the grid
			for(BoardNode b : visited)
			{
				this.grid[b.getRow()][b.getCol()] = new BoardNode(b.getRow(), b.getCol(), this.grid[b.getRow()][b.getCol()].getValue(), BNT.EMPTY);
			}
		}
	}
	
	/**
	 * Retrieve the BoardNode at the given position
	 * @param x position of the node to modify
	 * @param y position of the node to modify
	 * @return the BoardNode at the given position
	 * @throws BoardException
	 */
	public BoardNode get(int x, int y) throws BoardException
	{
		if(x<0 || x>=this.width || y<0 || y>=this.height)
			throw new BoardException();
		return grid[x][y];
	}
	
	/**
	 * Change the type of a BoardNode in the grid
	 * @param x position of the node to modify
	 * @param y position of the node to modify
	 * @param type new type of the node : BNT {EMPTY, BLACK, WHITE}
	 * @throws BoardException if x or y are out of bound
	 */
	public void set(int x, int y, BNT type) throws BoardException
	{
		if(x<0 || x>=this.width || y<0 || y>=this.height)
			throw new BoardException();
		this.grid[x][y].setType(type) ;
	}
	
	/**
	 * Plays a move
	 * @param x position of the new rock
	 * @param y position of the new rock
	 * @throws BoardException if x or y are out of bound
	 */
	public void playMove(int x, int y) throws BoardException
	{
		this.set(x, y, nextPlayerWhite ? BNT.WHITE : BNT.BLACK);
		nextPlayerWhite = !nextPlayerWhite;
	}
	
	/**
	 * @return the width of the grid
	 */
	public int getWidth() {
		return width;
	}

	/**
	 * @param width the width of the grid to set
	 */
	public void setWidth(int width) {
		this.width = width;
	}

	/**
	 * @return the height of the grid
	 */
	public int getHeight() {
		return height;
	}

	/**
	 * @param height the height of the grid to set
	 */
	public void setHeight(int height) {
		this.height = height;
	}

	/**
	 * @return the grid
	 */
	public BoardNode[][] getGrid() {
		return grid;
	}

	/**
	 * @param grid the grid to set
	 */
	public void setGrid(BoardNode[][] grid) {
		this.grid = grid;
	}

	/**
	 * @return the nextPlayerWhite
	 */
	public boolean isNextPlayerWhite() {
		return nextPlayerWhite;
	}
}
